1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.assets.textureatlas;
12 public import hip.api.data.textureatlas;
13 import hip.assets.texture;
14 import hip.api.data.asset;
15 
16 
17 class HipTextureAtlas : HipAsset, IHipTextureAtlas
18 {
19     import hip.assets.image;
20     string atlasPath;
21     string[] texturePaths;
22     IHipTexture texture;
23     AtlasFrame[string] _frames;
24 
25     ref inout(AtlasFrame[string]) frames() inout {return _frames;}
26 
27     this()
28     {
29         super("TextureAtlas");
30         _typeID = assetTypeID!HipTextureAtlas;
31     }
32 
33     string getTexturePath () const
34     {
35         return texturePaths[0];
36     }
37 
38 
39     bool loadTexture (in Image image)
40     {
41         import hip.assets.texture;
42         texture = new HipTexture(image);
43         if(!texture.hasSuccessfullyLoaded)
44             return false;
45         foreach(k, ref v; frames)
46         {
47             v.region = new HipTextureRegion(texture,
48                 cast(uint)v.frame.x, cast(uint)v.frame.y,
49                 cast(uint)(v.frame.x + v.frame.width),
50                 cast(uint)(v.frame.y + v.frame.height)
51             );
52         }
53         return true;
54     }
55 
56     static HipTextureAtlas readJSON (const ubyte[] data, string atlasPath, string texturePath)
57     {
58         import hip.data.json;
59         import hip.assets.texture;
60         HipTextureAtlas ret = new HipTextureAtlas();
61         ret.texturePaths~= texturePath;
62         ret.atlasPath = atlasPath;
63 
64         import hip.console.log;
65 
66         JSONValue json = parseJSON(cast(string)data);
67         if(json["frames"].type == JSONType.array)
68         {
69             foreach(f; json["frames"].array)
70             {
71                 AtlasFrame a;
72                 a.filename = f["filename"].str;
73                 a.rotated  = f["rotated"].boolean;
74                 a.trimmed  = f["trimmed"].boolean;
75                 JSONValue frameRect = f["frame"].object;
76                 a.frame = AtlasRect(
77                     cast(uint)frameRect["x"].integer,
78                     cast(uint)frameRect["y"].integer,
79                     cast(uint)frameRect["w"].integer,
80                     cast(uint)frameRect["h"].integer
81                 );
82                 frameRect = f["spriteSourceSize"].object;
83                 a.spriteSourceSize = AtlasRect(
84                     cast(uint)frameRect["x"].integer,
85                     cast(uint)frameRect["y"].integer,
86                     cast(uint)frameRect["w"].integer,
87                     cast(uint)frameRect["h"].integer
88                 );
89                 frameRect = f["sourceSize"].object;
90                 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer);
91                 ret.frames[a.filename] = a;
92             }
93         }
94         else
95         {
96             JSONValue frames = json["frames"].object;
97             JSONValue meta = json["meta"].object;
98             foreach(string frameName, JSONValue f; frames)
99             {
100                 AtlasFrame a;
101                 a.filename = frameName;
102                 a.rotated  = f["rotated"].boolean;
103                 a.trimmed  = f["trimmed"].boolean;
104                 JSONValue frameRect = f["frame"].object;
105                 a.frame = AtlasRect(
106                     cast(uint)frameRect["x"].integer,
107                     cast(uint)frameRect["y"].integer,
108                     cast(uint)frameRect["w"].integer,
109                     cast(uint)frameRect["h"].integer
110                 );
111                 frameRect = f["spriteSourceSize"].object;
112                 a.spriteSourceSize = AtlasRect(
113                     cast(uint)frameRect["x"].integer,
114                     cast(uint)frameRect["y"].integer,
115                     cast(uint)frameRect["w"].integer,
116                     cast(uint)frameRect["h"].integer
117                 );
118                 frameRect = f["sourceSize"].object;
119                 a.sourceSize = AtlasSize(cast(uint)frameRect["w"].integer, cast(uint)frameRect["h"].integer);
120                 ret.frames[frameName] = a;
121             }
122         }
123 
124         return ret;
125     }
126 
127     /**
128     *   If no texturePath is given, it will try spritesheetPath<.png>
129     *   I found a txt file that is parsed as:
130     *
131 `spriteName = x y width height`
132     */
133     // static HipTextureAtlas readSpritesheet (string spritesheetPath, string texturePath = "")
134     // {
135     //TODO: FIX HIPFS
136     //     import hip.filesystem.hipfs;
137     //     string data;
138     //     if(!HipFS.readText(spritesheetPath))
139     //     {
140     //         import hip.error.handler;
141     //         ErrorHandler.showWarningMessage("Could not find spritesheet from path ", spritesheetPath);
142     //         return null;
143     //     }
144     //     import hip.util.path;
145     //     if(texturePath == "")
146     //     {
147     //         texturePath = spritesheetPath.dup.extension(".png");
148     //     }
149 
150     //     return readSpritesheet(cast(ubyte[])data, spritesheetPath, texturePath);
151     // }
152 
153     static HipTextureAtlas readSpritesheet (const ubyte[] data, string spritesheetPath, string texturePath)
154     {
155         import hip.util.string:splitRange, trim, isNumber;
156         import hip.assets.texture;
157 
158         string toParse = cast(string)data;
159 
160         HipTextureAtlas atlas = new HipTextureAtlas();
161         atlas.atlasPath = spritesheetPath;
162         atlas.texturePaths~= texturePath;
163 
164 
165         foreach(line; splitRange(toParse, "\n"))
166         {
167             import hip.util.algorithm;
168             import hip.util.conv;
169             import hip.console.log;
170             line = line.trim();
171             auto lineDataRange = splitRange(line, " ");
172             lineDataRange.popFront;
173             string frame = lineDataRange.front;
174             if(frame != "")
175             {
176                 while(!lineDataRange.empty && !lineDataRange.front.isNumber)
177                 {
178                     lineDataRange.popFront; //Find a number
179                 }
180                 int x = void, y = void, width = void, height = void;
181                 lineDataRange.map((string data) => to!int(data)).put(&x, &y, &width, &height);
182                 AtlasFrame atlasFrame;
183                 atlasFrame.spriteSourceSize = AtlasRect(x, y, width, height);
184                 atlasFrame.frame = AtlasRect(x, y, width, height);
185                 atlasFrame.sourceSize = AtlasSize(width, height);
186                 atlasFrame.filename = frame;
187                 atlas.frames[frame] = atlasFrame;
188             }
189         }
190         return atlas;
191     }
192 
193 
194     static HipTextureAtlas readFromMemory (const ubyte[] data, string atlasPath, string texturePath = ":IGNORE")
195     {
196         import hip.util.path;
197         switch(atlasPath.extension)
198         {
199             case "xml":
200                 return HipTextureAtlas.readXML(data, atlasPath, texturePath);
201             case "atlas":
202                 return HipTextureAtlas.readAtlas(data, atlasPath);
203             case "json":
204                 return HipTextureAtlas.readJSON(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath);
205             default:
206                 return HipTextureAtlas.readSpritesheet(data, atlasPath, texturePath == ":IGNORE" ? "" : texturePath);
207         }
208     }
209 
210     /**
211     *   Used for the following type of XML (Parsed without a real XML parser):
212     ```xml
213        <TextureAtlas imagePath="image.png">
214            <SubTexture name="sub.png" x="0" y="0" width="0" height="0"/>
215        </TextureAtlas>
216     ```
217     */
218     static HipTextureAtlas readXML (const ubyte[] data, string atlasPath, string texturePath = ":IGNORE")
219     {
220         import hip.assets.texture;
221         import hip.util.string;
222         import hip.util.path;
223         import hip.util.conv;
224         string dataToParse = cast(string)data;
225         import hip.console.log;
226         //TODO: Fix .after (as it only executes startsWith and is returning null)
227         dataToParse = dataToParse.findAfter("imagePath=");
228         if(texturePath == ":IGNORE")
229             texturePath = atlasPath.dup.extension(".png");
230         else
231             texturePath = dataToParse.between("\"", "\"");
232         HipTextureAtlas atlas = new HipTextureAtlas();
233         atlas.texturePaths~= texturePath;
234         dataToParse = dataToParse.findAfter(">");
235 
236         foreach(line; dataToParse.splitRange("\n"))
237         {
238             line = line.trim();
239             if(!line)
240                 continue;
241             if(line.startsWith("</TextureAtlas>"))
242                 break;
243             string name = (line = line.findAfter("name=")).between(`"`, `"`);
244             int x = (line = line.findAfter("x=")).between(`"`, `"`).to!int;
245             int y = (line = line.findAfter("y=")).between(`"`, `"`).to!int;
246             int width = (line = line.findAfter("width=")).between(`"`, `"`).to!int;
247             int height = (line = line.findAfter("height=")).between(`"`, `"`).to!int;
248 
249             AtlasFrame frame;
250             frame.filename = name;
251             frame.frame = AtlasRect(x, y, width, height);
252             frame.spriteSourceSize = AtlasRect(x, y, width, height);
253             frame.sourceSize = AtlasSize(width, height);
254             atlas.frames[frame.filename] = frame;
255         }
256         return atlas;
257     }
258 
259 
260     static HipTextureAtlas readAtlas (const ubyte[] data, string atlasPath)
261     {
262         import hip.util.string : split, countUntil;
263         import hip.util.conv : to;
264 
265         HipTextureAtlas ret = new HipTextureAtlas();
266         ret.atlasPath = atlasPath;
267         string atlasFile = cast(string)data;
268 
269         string[] lines = atlasFile.split("\n");
270         int i = 0;
271         while(lines[i] == "")
272             i++;
273         string textureName = lines[i++];
274         ret.texturePaths~= textureName;
275         string sizeText = lines[i++];
276         string format = lines[i++];
277         string filter = lines[i++];
278         string repeat = lines[i++];
279 
280         const int offset = i;
281 
282         for(; i < lines.length-offset; i+= 7)
283         {
284             AtlasFrame frame;
285             frame.trimmed = false;
286             frame.filename = lines[i];
287 
288             string rotate = lines[i+1];
289                 rotate = rotate[rotate.countUntil(":")+2 .. $];
290             frame.rotated = to!bool(rotate);
291 
292             string xy = lines[i+2];
293                 xy = xy[xy.countUntil(":")+2 .. $];
294 
295             ptrdiff_t commaIndex = xy.countUntil(',');
296             int x = to!int(xy[0..commaIndex]);
297             //To account space must increate 2
298             int y = to!int(xy[commaIndex+2..$]);
299 
300             string size = lines[i+3];
301                 size = size[size.countUntil(":")+2 .. $];
302 
303             commaIndex = size.countUntil(',');
304             int sizeW = to!int(size[0..commaIndex]);
305             int sizeH = to!int(size[commaIndex+2..$]);
306 
307             string orig = lines[i+4];
308                 orig = orig[orig.countUntil(":")+2 .. $];
309 
310             commaIndex = orig.countUntil(',');
311             int origX = to!int(orig[0..commaIndex]);
312             int origY = to!int(orig[commaIndex+2..$]);
313 
314             string _offset = lines[i+5];
315                 _offset = _offset[_offset.countUntil(":")+2 .. $];
316 
317             commaIndex = _offset.countUntil(',');
318             int _offsetX = to!int(_offset[0..commaIndex]);
319             int _offsetY = to!int(_offset[commaIndex+2..$]);
320 
321             string index = lines[i+6];
322                 index = index[index.countUntil(":")+2 .. $];
323 
324             frame.frame = AtlasRect(x, y, sizeW, sizeH);
325             frame.spriteSourceSize = AtlasRect(_offsetX, _offsetY, sizeW, sizeH);
326             frame.sourceSize = AtlasSize(sizeW, sizeH);
327             ret.frames[frame.filename] = frame;
328         }
329         return ret;
330     }
331 
332 
333     override void onFinishLoading(){}
334     override void onDispose(){}
335     override bool isReady() const {return texture !is null && frames.length > 0;}
336 
337 
338     alias frames this;
339 }